package com.ting.encrypt.interceptor;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.MybatisParameterHandler;
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import com.ting.encrypt.annotation.EncryptField;
import com.ting.encrypt.annotation.SensitiveEncryptEnabled;
import com.ting.encrypt.config.EncryptConfig;
import com.ting.encrypt.entity.FieldAndIndexEntity;
import com.ting.encrypt.utils.EncryptUtil;
import com.ting.encrypt.utils.JsqlparserUtil;
import lombok.extern.slf4j.Slf4j;
import net.jodah.expiringmap.ExpiringMap;
import org.apache.commons.collections.CollectionUtils;
import org.apache.ibatis.binding.MapperMethod;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.BoundSql;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.ParameterMapping;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.plugin.Interceptor;
import org.apache.ibatis.plugin.Intercepts;
import org.apache.ibatis.plugin.Invocation;
import org.apache.ibatis.plugin.Signature;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.apache.ibatis.session.Configuration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;

import java.lang.reflect.Field;
import java.sql.Connection;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Intercepts({
        @Signature(
                type = StatementHandler.class,
                method = "prepare",
                args = {Connection.class, Integer.class})
})
@Slf4j
public class EncryptInterceptor implements Interceptor {

    private static final String MAPPED_STATEMENT = "mappedStatement";
    private static final String MYBATIS_PLUS_SQL_PARAM_NAME_PREX = "MPGENVAL";
    private static final ExpiringMap<String, Set> paramMap;
    @Autowired
    ApplicationContext applicationContext;
    @Autowired
    EncryptUtil encryptUtil;
    static {
        paramMap = ExpiringMap.builder().expiration(30, TimeUnit.SECONDS).build();
    }
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        boolean isParamsEncrypt = false;
        String threadName = Thread.currentThread().getName();
        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
        MybatisParameterHandler parameterHandler =
                (MybatisParameterHandler) statementHandler.getParameterHandler();
        MetaObject metaObject2 = SystemMetaObject.forObject(parameterHandler);
        MappedStatement mappedStatement = (MappedStatement) metaObject2.getValue("mappedStatement");
        SqlCommandType sqlCommandType = mappedStatement.getSqlCommandType();
        // 只拦截select ，insert，update操作
        if (!SqlCommandType.SELECT.equals(sqlCommandType)
                && !SqlCommandType.INSERT.equals(sqlCommandType)
                && !SqlCommandType.UPDATE.equals(sqlCommandType)) {
            return invocation.proceed();
        }
        BoundSql boundSql = (BoundSql) metaObject2.getValue("boundSql");
        Configuration configuration = mappedStatement.getConfiguration();
        Object params = boundSql.getParameterObject();
        if (params == null) return invocation.proceed();
        // 判断params是否已被使用加密过
        Set<Object> paramSet = paramMap.getOrDefault(threadName, new HashSet<Object>());
        if (paramSet.contains(params)) {
            isParamsEncrypt = true;
        }
        if (params instanceof Map) {
            if (params instanceof MapperMethod.ParamMap) {
                MapperMethod.ParamMap paramMap = (MapperMethod.ParamMap) params;
                if (paramMap.containsKey("ew") || paramMap.containsKey("et")) {
                    if (paramMap.containsKey("ew")) { // 查询
                        // mybatis-plus QueryWarpper形式查询时的参数别名为ew
                        Object ewParams = paramMap.get("ew");
                        AbstractWrapper wrapper = (AbstractWrapper) ewParams;
                        Map paramNameValuePairs = wrapper.getParamNameValuePairs();
                        String sql = boundSql.getSql();
                        // 获取sql处理的表名
                        List<String> tableNames = JsqlparserUtil.getTableName(sql);
                        if(CollectionUtils.isEmpty(tableNames)) {
                            return invocation.proceed();
                        }
                        String tableName = tableNames.get(0);
                        boolean isOnlyWhere = !paramMap.containsKey("et")
                                || paramMap.get("et") != null;
                        // mapper.update(null,updateWrapper) 情况
                        List<FieldAndIndexEntity> cloumnNames =
                                JsqlparserUtil.getCloumnNames(sql, isOnlyWhere);
                        // 这个表中哪些字段需要加密
                        Map<String, EncryptField> encryptTableInfoMap =
                                EncryptConfig.getEncryptTableInfoMap(tableName);
                        if (encryptTableInfoMap != null && !isParamsEncrypt) {
                            for (FieldAndIndexEntity columnName : cloumnNames) {
                                String fieldName = columnName.getFieldName();
                                if (encryptTableInfoMap.containsKey(fieldName)) {
                                    List<Integer> index = columnName.getIndex();
                                    EncryptField encryptField = encryptTableInfoMap.get(fieldName);
                                    for (int i = 0; i < index.size(); i++) {
                                        String key =
                                                MYBATIS_PLUS_SQL_PARAM_NAME_PREX + index.get(i);
                                        Object value = paramNameValuePairs.get(key);
                                        paramNameValuePairs.put(
                                                key,
                                                encryptUtil.getEncryptValue(value, encryptField));
                                    }
                                }
                            }
                        }
                    }
                    if (paramMap.containsKey("et")) { // mybatis-plus api更新操作的参数
                        params = paramMap.get("et");
                        if (params != null) {
                            // 更新操作会改变入参对象得值，为了不改变入参对象得值（可能会将这个值再使用），深拷贝一个对象
                            String paramsJson = JSON.toJSONString(params);
                            params = JSON.parseObject(paramsJson, params.getClass());
                            paramMap.put("et", params);
                            if (paramMap.containsKey("param1")) {
                                paramMap.put("param1", params);
                            }
                        }
                    }
                } else {
                    String sql = boundSql.getSql();
                    if (!sql.contains(";")) {
                        // 获取sql处理的表名
                        List<String> tableNames = JsqlparserUtil.getTableName(sql);
                        List<FieldAndIndexEntity> cloumnNames =
                                JsqlparserUtil.getCloumnNames(sql, false);
                        for (String tableName : tableNames) {
                            // 这个表中哪些字段需要加密metaParameters = {MetaObject@6614}
                            Map<String, EncryptField> encryptTableInfoMap =
                                    EncryptConfig.getEncryptTableInfoMap(tableName);
                            if (encryptTableInfoMap != null) {
                                List<ParameterMapping> parameterMappings =
                                        boundSql.getParameterMappings();
                                for (FieldAndIndexEntity cloumnName : cloumnNames) {
                                    String fieldName = cloumnName.getFieldName();
                                    List<Integer> indexs = cloumnName.getIndex();
                                    if (encryptTableInfoMap.containsKey(fieldName)) {
                                        EncryptField encryptField =
                                                encryptTableInfoMap.get(fieldName);
                                        for (Integer index : indexs) {
                                            if (index <= parameterMappings.size()) { // index从1开始
                                                ParameterMapping parameterMapping =
                                                        parameterMappings.get(index - 1);
                                                String property =
                                                        parameterMapping
                                                                .getProperty(); // xml中占位符中的字段名
                                                MetaObject metaObject1 =
                                                        configuration.newMetaObject(params);
                                                if (metaObject1.hasGetter(property)
                                                        && !isParamsEncrypt) { // 判断是否有该属性
                                                    Object rawValue =
                                                            metaObject1.getValue(property);
                                                    Object newValue =
                                                            encryptUtil.getEncryptValue(
                                                                    rawValue, encryptField);
                                                    if (newValue != null
                                                            && !newValue.equals(rawValue))
                                                        metaObject1.setValue(property, newValue);
                                                } else {
                                                    Object rawValue =
                                                            boundSql.getAdditionalParameter(
                                                                    property);
                                                    if (rawValue instanceof String) {
                                                        boundSql.setAdditionalParameter(
                                                                property,
                                                                encryptUtil.getEncryptValue(
                                                                        rawValue, encryptField));
                                                    } else if (!isParamsEncrypt) {
                                                        MetaObject metaObject01 =
                                                                configuration.newMetaObject(
                                                                        rawValue);
                                                        Object rawvalue =
                                                                metaObject01.getValue(property);
                                                        Object newValue =
                                                                encryptUtil.getEncryptValue(
                                                                        metaObject01.getValue(
                                                                                property),
                                                                        encryptField);
                                                        if (newValue != null
                                                                && !newValue.equals(rawvalue))
                                                            metaObject01.setValue(
                                                                    property, newValue);
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            } else {
                return invocation.proceed();
            }
        }
        if (params == null) return invocation.proceed();
        // 参数对象上是否有 @SensitiveEncryptEnabled 注解，有则表示这个类支持数据加解密
        SensitiveEncryptEnabled encryptEnabled =
                params.getClass().getAnnotation(SensitiveEncryptEnabled.class);
        if (encryptEnabled != null && !isParamsEncrypt) {
            handlerParamter(configuration, params);
        }
        paramSet.add(params);
        paramMap.put(threadName, paramSet);
        return invocation.proceed();
    }
    private void handlerParamter(Configuration configuration, Object params) throws Exception {
        MetaObject metaObject = configuration.newMetaObject(params);
        for (Field field : params.getClass().getDeclaredFields()) {
            if (field.isSynthetic() || "serialVersionUID".equals(field.getName())) {
                continue;
            }
            Object value = metaObject.getValue(field.getName());
            Object newValue = value;
            // 如果是一个字符序列
            if (value instanceof CharSequence) {
                newValue = encryptUtil.handlerFieldEncrypt(field, value);
            }
            if (value != null && newValue != null && !value.equals(newValue)) {
                metaObject.setValue(field.getName(), newValue);
            }
        }
    }
}
